﻿<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>原生JS实现弹幕效果</title>
  <style>
    #wrapper {
      height: 400px;
      width: 700px;
      position: relative;
      overflow: hidden;
      background: url(http://www.drama-asia.se/wp-content/uploads/2016/06/14375197_1349947520504_800x600.jpg);
      
      color: #ffffff82;
      font-size: 14px;
      text-shadow: 1px 1px #000;
    }
    .right {
      position: absolute;
      visibility: hidden;
      white-space: nowrap;
      /*left: 700px;*/
      transform: translateX(700px);
    }
    .left {
      position: absolute;
      white-space: nowrap;
      user-select: none;
      transition: transform 7s linear; /* 时间相同 越长的弹幕滑动距离越长 所以越快~ */
    }
    input {
      position: absolute;
      bottom: 10px;
      left: 150px;
      width: 300px;
      height: 26px;
    }

    button {
      position: absolute;
      bottom: 8px;
      left: 476px;
      width: 100px;
      height: 38px;
      border-radius: 10px;
      font-size: 16px;
    }
  </style>
</head>
<body>
<div id="wrapper">
  <input type="text">
  <button>发&nbsp;&nbsp;送</button>
</div>
'前方大量弹幕来袭，请做好准备！', '2333333', '2333333', '2333333', '2333333', '2333333',
'浔阳江头夜送客， 枫叶荻花秋瑟瑟',  '2333333', '2333333', '2333333', '2333333', '2333333', '2333333',
'主人下马客在船， 举酒欲饮无管弦。', '醉不成欢惨将别， 别时茫茫江浸月', '忽闻水上琵琶声， 主人忘归客不发。',
'寻声暗问弹者谁？ 琵琶声停欲语迟。', '移船相近邀相见， 添酒回灯重开宴。', '千呼万唤始出来， 犹抱琵琶半遮面。',
'转轴拨弦三两声， 未成曲调先有情。', '弦弦掩抑声声思， 似诉平生不得志。', '低眉信手续续弹， 说尽心中无限事。',
'轻拢慢捻抹复挑， 初为霓裳后六幺。', '大弦嘈嘈如急雨， 小弦切切如私语。', '嘈嘈切切错杂弹， 大珠小珠落玉盘。',
'间关莺语花底滑， 幽咽泉流冰下难。', '冰泉冷涩弦凝绝， 凝绝不通声暂歇。', '别有幽愁暗恨生， 此时无声胜有声。',
'银瓶乍破水浆迸， 铁骑突出刀枪鸣。', '曲终收拨当心画， 四弦一声如裂帛。', '东船西舫悄无言， 唯见江心秋月白。',
'沉吟放拨插弦中， 整顿衣裳起敛容。', '自言本是京城女， 家在虾蟆陵下住。', '十三学得琵琶成， 名属教坊第一部。',
'曲罢曾教善才服， 妆成每被秋娘妒。', '五陵年少争缠头， 一曲红绡不知数。', '钿头银篦击节碎， 血色罗裙翻酒污。',
'今年欢笑复明年， 秋月春风等闲度。', '弟走从军阿姨死， 暮去朝来颜色故。', '门前冷落鞍马稀， 老大嫁作商人妇。',
'商人重利轻别离， 前月浮梁买茶去。', '去来江口守空船， 绕船月明江水寒。', '夜深忽梦少年事， 梦啼妆泪红阑干。',
'我闻琵琶已叹息， 又闻此语重唧唧。', '同是天涯沦落人， 相逢何必曾相识！', '我从去年辞帝京， 谪居卧病浔阳城。',
'浔阳地僻无音乐， 终岁不闻丝竹声。', '住近湓江地低湿， 黄芦苦竹绕宅生。', '其间旦暮闻何物？ 杜鹃啼血猿哀鸣。',
'春江花朝秋月夜， 往往取酒还独倾。', '岂无山歌与村笛？ 呕哑嘲哳难为听。', '今夜闻君琵琶语， 如听仙乐耳暂明。',
'莫辞更坐弹一曲， 为君翻作《琵琶行》。', '感我此言良久立， 却坐促弦弦转急。', '凄凄不似向前声， 满座重闻皆掩泣。',
'座中泣下谁最多？ 江州司马青衫湿。'
<script>
/**
 * 设置 弹幕DOM池 每一个通道最多六条弹幕
**/

const MAX_DM_COUNT = 6; //

const CHANNEL_COUNT = 10; // 弹幕的弹道【解决弹幕重叠】

let domPool = []; // DOM 池 存放span标签

let danmuPool = [

]; // 弹幕池
let hasPosition = [];

/**
 * 做一下初始化工作
 */
function init() {
  let wrapper = document.getElementById('wrapper');// 获取最大的容器

  // 先new一些span 重复利用这些DOM
  for (let j = 0; j < CHANNEL_COUNT; j++) { // 循环 弹道

    let doms = []; // 声明一个 DOM 数组

    for (let i = 0; i < MAX_DM_COUNT; i++) { // 循环

      // 要全部放进wrapper

      let dom = document.createElement('span'); // 生成一个span标签，存入dom 变量

      wrapper.appendChild(dom); // 把 span 标签 添加到 容器里

      // 初始化dom的位置 通过设置className
      dom.className = 'right'; // 为每个 span 标签 添加一个class【right】

      // DOM的 弹道 是固定的 所以设置好top就不需要再改变了
      dom.style.top = j * 20 + 'px';
      /*
        弹幕会自上而下
      * 0 * 20 => 0
      * 1 * 10 => 20
      * 2 * 20 => 40
      * 3 * 20 => 60
      * 4 * 20 => 80
      * 5 * 20 => 100
      * 6 * 20 => 120
      * 7 * 20 => 140
      *
      * */

      doms.push(dom);// 【span】放入改通道的doms 数组

      // 每次到transition结束的时候 就是弹幕划出屏幕了 将DOM位置重置 再放回DOM池
      dom.addEventListener('transitionend', () => { //transitionend事件：在 CSS 完成过渡【transitione】后触发
        /* transition: transform 7s linear; 也就是7s 过后*/

        dom.className = 'right'; // 重新为该dom【span】添加right

        // dom.style.transition = null;
        // dom.style.left = null;
        dom.style.transform = null; // 优化

        domPool[j].push(dom); // 把那些已经弹出屏幕的 弹幕重新添加到 DOM池

      });
    }

    domPool.push(doms); //把所有生成的 span 都放入 DOM池中
  }

  // hasPosition 标记每个弹道目前是否有位置
  for (let i = 0; i < CHANNEL_COUNT; i++) {

    hasPosition[i] = true;

  }
}

/**
 * 获取一个可以发射弹幕的弹道 没有则返回-1
 */
function getChannel() {
  for (let i = 0; i < CHANNEL_COUNT; i++) {
    if (hasPosition[i] && domPool[i].length) {

      return i
    }
    // 那一条弹道是可用     DOM 池
  }
  return -1;
}

/**
 * 根据DOM和弹幕信息 发射弹幕
 * 参数：1. dom span标签
 *      2. text 输入的弹幕
 *      3. channel 弹道
 */
function shootDanmu(dom, text, channel) { // span元素 弹幕 弹道
//  console.log('biu~ [' + text + ']');

  dom.innerText = text;
  // 如果为每个弹幕设置 transition 可以保证每个弹幕的速度相同 这里没有保证速度相同
  // dom.style.transition = `transform ${7 + dom.clientWidth / 100}s linear`;

  // dom.style.left = '-' + dom.clientWidth + 'px';
  // 设置弹幕的位置信息 性能优化 left -> transform
  dom.style.transform = `translateX(${ -dom.clientWidth }px)`;
  dom.className = 'left'; // 启动 transition: transform 7s linear;
  hasPosition[channel] = false; // 占用该 弹道

  // 弹幕【初始化弹幕池】全部显示之后 才能开始下一条弹幕
  // 大概 dom.clientWidth * 10 的时间 该条弹幕就从右边全部划出到可见区域 再加1秒保证弹幕之间距离
  
  setTimeout(() => { // 
    hasPosition[channel] = true;
  }, dom.clientWidth * 10 + 1000);
  // clientWidth 文本宽度

}

window.onload = function() {

  init();

  // 为input和button添加事件监听
  let btn = document.getElementsByTagName('button')[0];
  let input = document.getElementsByTagName('input')[0];

  btn.addEventListener('click', () => { // 单击按钮

    input.value = input.value.trim(); // 去除输入弹幕前后空格

    if (input.value) danmuPool.push(input.value); // 如果输入有内容，就把内容加入 弹幕池

  });

  input.addEventListener('keyup', (e) => { // 键盘事件 执行以上同样行为

    if (e.key === 'Enter' && (input.value = input.value.trim())) {

      danmuPool.push(input.value);

    }
  });

  // 每隔1ms从弹幕池里获取弹幕（如果有的话）并发射
  setInterval(() => {

    let channel;

    if (danmuPool.length && (channel = getChannel()) != -1) {
      // 弹幕池中是否有弹幕    获取一条弹道 赋值 channel变量

      let dom = domPool[channel].shift();

      let danmu = danmuPool.shift(); // 提出 弹幕池中的第一个元素

      shootDanmu(dom, danmu, channel);

    }
  }, 1);

}
 
</script>
</body>
</html>